/**
 * Klasseopmerkingen:
 * 		setThrusterActive
 */

package asteroids.model;


import java.util.*;

import ModelExceptions.*;
import asteroids.Util;
import asteroids.model.programs.Program;
import be.kuleuven.cs.som.annotate.*;

public class Ship extends CollisionObject{

	/**
	 * 
	 * @param xCoordinate	The current x-value of the ships center (2d)
	 * @param xCoordinate				The x-value of the ships center (2d)
	 * @param yCoordinate				The y-value of the ships center (2d)
	 * @param xVelocity					The rate at which the x-value changes
	 * @param yVelocity					The rate at which the y-value changes
	 * @param radius					The distance between the center of the ship and the outer rim
	 * @param angle						The direction that the ship is pointing at (2d)
	 * @param mass						The mass of the ship	
	 * @throws IllegalRadiusException	Thrown when the radius to assign is invalid
	 * 									|!isValidRadius
	 * @throws IllegalMassException		Thrown when the mass to assign is invalid
	 * 									|!isValidMass(mass)
	 */
	public Ship (double xCoordinate, double yCoordinate, double xVelocity, double yVelocity, double radius, double angle,double mass) throws IllegalMassException,IllegalRadiusException{
		super(xCoordinate, yCoordinate, xVelocity, yVelocity, radius);
		setAngle(angle);
		if(isValidMass(mass))
			setMass(mass);
	}


	/**
	 * Gives you the smallest allowed radius
	 * @return	The smallest radius that is allowed
	 */
	@Basic
	public double getRadiusConstraint(){
		return this.radiusConstraint;
	}
	private final double radiusConstraint = 10;




	/**
	 * Gives you the angle the ship makes in radians.
	 * @return	The angle the ship makes with the horizontal in radians
	 */
	@Basic
	public double getAngle(){
		return angle;
	}

	/**
	 * 
	 * @param angle	The direction that the ship is pointing at (2d)
	 * @pre			The angle must be a finite number
	 * 				|!Double.isInfinite(angle);
	 * @pre			The angle must be a valid number
	 * 				|!Double.isNaN(angle)
	 * @post		The given angle is the new angle
	 * 				|(new this).getAngle() == angle
	 */
	public void setAngle(double angle){
		assert !Double.isInfinite(angle);
		assert !Double.isNaN(angle);
		this.angle = angle;
	}

	/**
	 * Rotate the ship with the value of the givenAngle
	 * @param givenAngle	The angle over which your ship has to rotate
	 * @post	the new angle is the original angle adding up the given angle
	 * 			|(new this).getAngle() == angle + givenAngle 
	 */
	public void turn(double givenAngle){
		setAngle(getAngle() + givenAngle)	;
	}

	private double angle;

	/**
	 * Turn the thruster on or of by giving the true or false
	 * @param thrusterActive	True: turns the thruster on
	 * 							false: turns the thrust off
	 * @post	The ship's thruster is enabled or disabled.
	 * 			| if(thrusterActive == true)
	 * 			|	new.isShipThrusterActive()==true
	 * 			| else
	 * 			|	new.isShipThrusterActive()==false
	 */
	public void setThrusterActive(boolean active){
		this.thruster = active;
	}


	/**
	 * States whether or not the ship's thruster is enabled or disabled
	 * @return	True: The ships thruster is enabled
	 * 			False: The ships thruster is disabled
	 */
	@Basic
	public boolean isShipThrusterActive(){
		return this.thruster;
	}

	/**
	 * 
	 * @param duration	The time that a method gets
	 * @return	if the given duration is larger then zero, then result is duration
	 * 			otherwise the duration becomes zero
	 * 			|if(duration>0){result == true}
	 */
	private boolean isValidDuration(double duration){
		if(	Util.fuzzyLessThanOrEqualTo(duration, 0) ==true){return false;}
		else return true;
	}

	/**
	 * 
	 * @param a		The duration of the boost
	 * @post	If the new velocity is valid then, the new velocity is the old velocity +the given velocity, scaled with the angle of the ship
	 * 			|if(isValidVelocity(xVelocity+ a*cos(angle),yVelocity + a*sin(angle))) then
	 * 			|	(new this).getXVelocity() == xVelocity + a*cos(angle)
	 * 			|	(new this).getYVelocity() == yVelocity + a*sin(angle)}
	 * @post	If the velocity isn't valid, then the new velocity is equal to the velocityConstraint
	 * 			|if(!isValidVelocity(xVelocity+ a*cos(angle),yVelocity + a*sin(angle))) then
	 * 			|	velocityToAdd = velocityToAdd(xVelocity(), yVelocity())
	 * 			|	(new this).getXVelocity() == xVelocity() + velocityToAdd
	 * 			|	(new this).getYVelocity() == yVelocity() + velocityToAdd
	 */
	public void thrust(double dt){
		if(!isValidDuration(dt))
			throw new IllegalValueException("Illegal time amount");
		double xAcceleration =getXAcceleration(dt)*dt;
		double yAcceleration =getYAcceleration(dt)*dt;
		double newXVelocity = getXVelocity() + dt*xAcceleration;
		double newYVelocity = getYVelocity() + dt*yAcceleration;
		if(isValidVelocity(newXVelocity,newYVelocity)){
			setValidVelocity(newXVelocity,newYVelocity);
		}
		else{
			setVelocityToSpeedOfLight();
		}
	}




	/**
	 * Returns you the force the thruster of this ship exerts.
	 * @return The force the thruster of this ship exerts
	 */
	@Basic
	public double getThrusterForce() {
		return thrusterForce;
	}

	/**
	 * The total acceleration that this ship experiences due to his thruster
	 * @return 	The total acceleration that this ship experiences. 
	 * 			If the ship's thruster is disabled this ship does not experience a force from it's thruster
	 * 			|	result == getThrusterForce()/getMass()
	 * 
	 */
	public double getTotalAcceleration(double dt){
		return dt*getThrusterForce()/getMass();
	}

	/**
	 * The horizontal acceleration this ship experiences due to his thruster
	 * @return	The horizontal acceleration this ship experiences due to his thruster. The horizontal component from the totalAcceleration
	 * 			| getTotalAcceleration()*math.cos(getAngle())
	 */
	public double getXAcceleration(double dt){
		//A force is only exerted in the direction of the angle
		return getTotalAcceleration(dt)*Math.cos(this.getAngle());
	}

	/**
	 * The vertical acceleration this ship experiences due to his thruster
	 * @return	The vertical acceleration this ship experiences due to his thruster. The vertical component from the totalAcceleration
	 * 			| getTotalAcceleration()*math.sin(getAngle())
	 */
	public double getYAcceleration(double dt){
		return getTotalAcceleration(dt)*Math.sin(this.getAngle());
	}

	/**
	 * Assign a new force that the ship's thruster can exert.
	 * @param thrusterForce The new force this ship can exert to move
	 * @post	The new force this ship's thruster can exert is equal to the given value of thrusterForce
	 * 			|new.getThrusterForce() == thrusterForce
	 */
	public void setThrusterForce(double thrusterForce) {
		this.thrusterForce = thrusterForce;
	}

	private double thrusterForce = 1.1*Math.pow(10,21);
	private boolean thruster; 


	/**
	 * The ship will fire a bullet which will find it's origin at the border of this ship.
	 * @post	The bullet fired will be added to this ship as source
	 * 			| Bullet bullet = new Bullet(	this.getXCoordinate() + Math.cos(this.getAngle())*this.getRadius(),
				|	this.getYCoordinate() + Math.sin(this.getAngle())*this.getRadius(), 
				|	getInitialBulletSpeed()*Math.cos(this.getAngle()), 
				|	getInitialBulletSpeed()*Math.sin(this.getAngle()), 
				|	getStandardBulletRadius(), 
				|	this);
				| this.hasAsBullet(bullet)
				| && bullet.getShip() == this
	 * @throws IllegalStateException	The ship can only fire if it's ready to fire, if not this exception is thrown
	 * 									|!isReadyToFire()
	 * @throws IllegalBulletException	Thrown is the bullet fired is in invalid
	 * 									|Bullet bullet = new Bullet(	this.getXCoordinate() + Math.cos(this.getAngle())*this.getRadius(),
	 *									|	this.getYCoordinate() + Math.sin(this.getAngle())*this.getRadius(), 
	 *									|	getInitialBulletSpeed()*Math.cos(this.getAngle()), 
	 *									|	getInitialBulletSpeed()*Math.sin(this.getAngle()), 
	 *									|	getStandardBulletRadius(), 
	 *									|	this);
	 * 									|!bullet.isValidBullet()
	 */
	public void fireBullet() throws IllegalStateException, IllegalBulletException{
		if(isReadyToFire()){
			Bullet bullet = new Bullet(	this.getXCoordinate() + Math.cos(this.getAngle())*this.getRadius(),
					this.getYCoordinate() + Math.sin(this.getAngle())*this.getRadius(), 
					getInitialBulletSpeed()*Math.cos(this.getAngle()), 
					getInitialBulletSpeed()*Math.sin(this.getAngle()), 
					getStandardBulletRadius(), 
					this);
			if(bullet.isValidBullet()){
				this.addAsBullet(bullet);
			}

			else
				throw new IllegalBulletException("The created bullet is invalid");
		}

		else{
			throw new IllegalStateException();
		}
	}


	/**
	 * Returns a set of all the bullets fired by this ship that are not destroyed. 
	 * @return	A set of all the bullets fired by this ship that are not destroyed.
	 */
	//CLONE TERUGGEVEN
	@Basic @Raw
	public Set<Bullet> getBullets(){
		Set <Bullet> bullet = this.shipFiredBullets;
		return bullet;
	}

	/**
	 * Check whether or not this ship has fired the given bullet
	 * @param bullet The, not destroyed, bullet to check whether or not it has been fired by this ship.
	 * @return	true if the ship has fired this bullet and the bullet is not yet destroyed, false if this ship has not fired this bullet
	 * 			|getBullets().contains(bullet)
	 */
	@Basic
	public boolean hasAsBullet(Bullet bullet){
		return getBullets().contains(bullet);
	}

	/**
	 * Remove the given bullet from the bulletsset from this ship
	 * @param bullet	The bullet to remove
	 * @post		This ship does not own this bullet anymore
	 * 				|!new.hasAsBullet(bullet)
	 */
	@Raw
	public void removeAsBullet(@Raw Bullet bullet){
		if(hasAsBullet(bullet)){
			this.shipFiredBullets.remove(bullet);
			//			if(!bullet.isDestroyed())
			//				bullet.destroy();
		}
		//		else				//else do nothing
		//			throw new IllegalBulletException("This bullet does not exist");
	}


	/**
	 * Add a bullet to this ship. 
	 * @param bullet The bullet to add
	 * @post	This ship owns the given bullet of the given bullet can be added to this ship. Else the bullet is not added
	 * 			|if(canHaveAsBullet(bullet)
	 * 			|	new.hasAsBullet(bullet) == true
	 * 			|else
	 * 			|	new.hasAsBullet(bullet) == false
	 */
	public void addAsBullet(Bullet bullet){
		if(canHaveAsBullet(bullet)){
			this.shipFiredBullets.add(bullet);
		}
	}

	/**
	 * Check whether or not this ship can own the given bullet
	 * @param bullet	The bullet to check whether or not this ship can own it
	 * @return	true if this ship can own this bullet. 
	 * 			|bullet!=null && !bullet.isDestroyed() && ! bullet.isInNoWorld())
	 */
	public boolean canHaveAsBullet(Bullet bullet){
		if(bullet !=null && !bullet.isDestroyed() &&!bullet.isInNoWorld())
			return true;
		return false;
	}

	/**
	 * Check whether or not the bullets that this ship owns are all proper bullets and this ship is allowed to own them 
	 * and the bullet can have this ship as mothership.
	 * @return	False if this ship does not have all proper bullets
	 * 			|if(bullet.getShip()!=this) && !this.canHaveAsBullet(bullet) && !bulletcanHaveAsSourceShip(this)
	 */
	public boolean hasProperBullets(){
		for(Bullet bullet: shipFiredBullets){
			if( (bullet.getShip()!=this) && (!this.canHaveAsBullet(bullet)) && !bullet.canHaveAsSourceShip(this))
				return false;
		}
		return true;

	}

	/**
	 * The amount of bullets fired by this ship
	 * @return	The amount of bullets
	 */
	@Basic
	public int getNbOfBulletsFired(){
		return getBullets().size();
	}
	/**
	 * The standard radius that is used to create bullets
	 * @return The standardradius that can be used to create default bullets
	 */
	@Basic
	public double getStandardBulletRadius() {
		return standardBulletRadius;
	}


	/**
	 * Check whether or not this ship is allowed to fire
	 * @return	True if this ship can fire, false if not
	 * 			| isInWorld() && !isDestroyed()
	 */
	public boolean isReadyToFire(){
		if(isInWorld() && !isDestroyed() && this.getBullets().size() < getMaxNbOfBullets())
			return true;
		return false;
	}
	private double getMaxNbOfBullets(){
		return this.maxNbOfBullets;
	}
	private final double maxNbOfBullets = 3;

	/**
	 * Get the speed of a fired bullet when the bullet leaves this ship
	 * @return	The speed of the fired bullet
	 */
	@Basic
	public double getInitialBulletSpeed(){
		return this.initialBulletSpeed;
	}

	private final Set <Bullet> shipFiredBullets = new HashSet<Bullet>();
	private double standardBulletRadius = 3;
	private final double initialBulletSpeed = 250;


	/**
	 * Destroy this ship.
	 * @post	The sourceship from the bullets fired by this ship will be updated.
	 * 			|for each bullet in getBullets()
	 * 			| 	bullet.shipDestroy()
	 * @post	This ship will not own any bullets anymore
	 * 			| 	new.getNbOfBullets == 0;
	 * @post	The object is completely destroyed
	 * 			|	super.destroy()
	 * 			
	 */
	@Override
	public void destroy(){
		for(Bullet bullet: shipFiredBullets){
			//shipFiredBullets.remove(bullet);
			bullet.shipDestroy();
		}
		shipFiredBullets.clear();
		super.destroy();
	}


	/**
	 * The reaction that this ship will make with other collisionObjects
	 * @post...	
	 * 		|if(collisionObject.isShip()){
	 *		|this.bounceOffObject(collisionObject);
	 *		|}
	 *@post	|if(collisionObject.isBullet()){
	 *		|if(((Bullet) collisionObject).getBulletSource() != this && ((Bullet) collisionObject).getBulletSource() != null){
	 *		|	collisionObject.destroy();
	 *		|	this.destroy();
	 *		|}
	 *		|else{collisionObject.destroy();}
	 *		|}
	 *@post	|if(collisionObject.isAsteroid()){
	 *		|this.destroy();
	 *		|}
	 */
	@Override
	public void collisionReactionWith(CollisionObject collisionObject) {
		if(collisionObject.isShip()){
			this.bounceOffObject(collisionObject);
			//collisionObject.bounceOffObject(this);
		}
		if(collisionObject.isBullet()){
			if(((Bullet) collisionObject).getBulletSource() != this && ((Bullet) collisionObject).getBulletSource() != null){
				collisionObject.destroy();
				this.destroy();
			}
			else{collisionObject.destroy();}
		}
		if(collisionObject.isAsteroid()){
			this.destroy();
		}
	}

	@Override
	public String toString(){
		return "This is a ship with \n"+
				super.toString();
	}

	@Override
	public CollisionObject clone() {
		Ship ship = new Ship(getXCoordinate(), getYCoordinate(), getXVelocity(), getYVelocity(), getRadius(), getAngle(), getMass());
		ship.setWorld(this.getWorld());
		return ship;
	}


}


